﻿//-----------------------------------------------------------------------
// <copyright file="HtmltoXamlConverter.cs" company="AFS">
// Copyright (c) AFS. All rights reserved.
//
//    This file is part of Chatbox.
//
//    Chatbox is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    any later version.
//
//    Chatbox is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with Chatbox. If not, see http://www.gnu.org/copyleft/lesser.html
// </copyright>
//----------------------------------------------------------------------- 

namespace ChatBox.XamlUtility
{
    using System;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Windows.Documents;

    using ChatBox.Interfaces;
    using ChatBox.Utility;

    /// <summary>
    /// class to convert html to xaml
    /// </summary>
    public class HtmlToXamlConverter
    {
        /// <summary>
        /// How large to make the font for small messages, relative to the the default
        /// </summary>
        public const double SmallFontScale = 0.80;

        /// <summary>
        ///  the root of the html parse tree
        /// </summary>
        private readonly TagItem tagRoot;

        /// <summary>
        /// How to convert the tags
        /// </summary>
        private readonly TagMap tagMap;

        /// <summary>
        /// function to apply to text data
        /// </summary>
        private readonly Func<string, string> plaintextConverter;

        /// <summary>
        /// the root of the generated caml contents
        /// </summary>
        private Inline xamlContent;

        /// <summary>
        /// The error log
        /// </summary>
        private ILogger logger;

        /// <summary>
        /// Initializes a new instance of the HtmlToXamlConverter class
        /// </summary>
        /// <param name="htmlText">the text to convert</param>
        /// <param name="hyperlinkContext">how to set up hyperlinks</param>
        /// <param name="logger">log to send errors to</param>
        /// <param name="plaintextConverter">how to convert plaintext</param>
        public HtmlToXamlConverter(string htmlText, HyperlinkContext hyperlinkContext, ILogger logger, Func<string, string> plaintextConverter)
        {
            this.tagMap = new TagMap(hyperlinkContext);
            this.logger = logger;
            this.plaintextConverter = plaintextConverter;
            this.tagRoot = new TagItem(htmlText);

            this.GenerateXaml();
        }

        /// <summary>
        /// Initializes a new instance of the HtmlToXamlConverter class
        /// </summary>
        /// <param name="htmlText">the text to convert</param>
        public HtmlToXamlConverter(string htmlText)
        {
            this.tagMap = new TagMap(null);
            this.tagRoot = new TagItem(htmlText);

            this.GenerateXaml();
        }

        /// <summary>
        /// Gets the xaml made from the html
        /// </summary>
        public Inline XamlContent
        {
            get 
            {
                return this.xamlContent; 
            }
        }

        /// <summary>
        /// convert html to xaml
        /// </summary>
        /// <param name="htmlText">the text to convert</param>
        /// <param name="hyperlinkContext">how to set up hyperlinks</param>
        /// <param name="logger">where to log errors</param>
        /// <param name="plaintextConverter">effect to apply to plain text</param>
        /// <returns>the xaml content</returns>
        public static Inline Convert(string htmlText, HyperlinkContext hyperlinkContext, ILogger logger, Func<string, string> plaintextConverter)
        {
            HtmlToXamlConverter instance = new HtmlToXamlConverter(htmlText, hyperlinkContext, logger, plaintextConverter);
            return instance.XamlContent;
        }

        /// <summary>
        /// convert html to xaml
        /// </summary>
        /// <param name="htmlText">the text to convert</param>
        /// <returns>the xaml content</returns>
        public static Inline Convert(string htmlText)
        {
            HtmlToXamlConverter instance = new HtmlToXamlConverter(htmlText);
            return instance.XamlContent;
        }

        /// <summary>
        /// Extract the html attributes from the tag start
        /// </summary>
        /// <param name="tag">the tag to process</param>
        /// <returns>the attributes</returns>
        private static string ExtractTagAttributes(string tag)
        {
            if (tag.Contains(" "))
            {
                return tag.After(" ").TrimSuffix(">");
            }
                
            return string.Empty;
        }

        /// <summary>
        /// Generate  xaml content from the message that could not be parsed
        /// </summary>
        /// <param name="tagItem">the tag to process</param>
        /// <returns>the XAML content</returns>
        private static Inline GenerateUnparsableContent(TagItem tagItem)
        {
            Span container = new Span();
            container.Inlines.Add(new Run("Unparsable message: "));
            container.Inlines.Add(new Run(tagItem.TextContent));

            return container;
        }

        /// <summary>
        /// Gnerate xaml from html text
        /// </summary>
        private void GenerateXaml()
        {
            try
            {
                this.ParseTag(this.tagRoot);
                this.xamlContent = this.RecursiveGenerateXaml(this.tagRoot);
            }
            catch (Exception ex)
            {
                this.xamlContent = GenerateUnparsableContent(this.tagRoot);

                // log the error
                if (this.logger != null)
                {
                    string message = "HTML to XAML parse failed on " + this.tagRoot.TextContent;
                    this.logger.LogException(message, ex);
                }
            }
        }

        /// <summary>
        /// parse the html in the tag item
        /// </summary>
        /// <param name="tagItem">the html text to parse</param>
        private void ParseTag(TagItem tagItem)
        {
            // find the first tag in the html inside the tag item
            Regex tagStartPattern = new Regex("<.*?>");

            if (!string.IsNullOrEmpty(tagItem.TextContent))
            {
                Match tagStartMatch = tagStartPattern.Match(tagItem.TextContent);

                if (tagStartMatch.Success)
                {
                    string tagStart = tagStartMatch.Value;
                    string tagName = tagStart;
                    string tagAttributes = ExtractTagAttributes(tagStart);
                    if (!string.IsNullOrEmpty(tagAttributes))
                    {
                        // the tag name is the tag without attributes
                        // e.g. reduce <a href="foo"> to <a>
                        tagName = tagStart.Replace(" " + tagAttributes, string.Empty);
                    }

                    bool hasEndTag = true;
                    if (tagName.EndsWith("/>", StringComparison.OrdinalIgnoreCase))
                    {
                        // tag is self-closing
                        tagName = tagName.Replace("/>", ">");
                        hasEndTag = false;
                    }

                    // turn the text into three parts - before, in and after the tag
                    string textBefore = tagItem.TextContent.Before(tagStart);
                    string textRest = tagItem.TextContent.MatchOnwards(tagStart);

                    // work out what ends the tag
                    string tagEnd = tagName.Replace("<", "</");
                    if (hasEndTag)
                    {
                        hasEndTag = textRest.Contains(tagEnd);
                    }

                    string textMid = hasEndTag ? textRest.UpToMatch(tagEnd) : textRest;
                    string textEnd = hasEndTag ? textRest.After(tagEnd) : string.Empty;

                    if (!string.IsNullOrEmpty(textBefore))
                    {
                        tagItem.ContainedTags.Add(new TagItem(textBefore));
                    }

                    tagItem.ContainedTags.Add(
                            new TagItem(
                                    textMid.TrimPrefixSuffix(tagStart, tagEnd),
                                    tagName.TrimPrefixSuffix("<", ">"),
                                    tagAttributes));

                    if (!string.IsNullOrEmpty(textEnd))
                    {
                        tagItem.ContainedTags.Add(new TagItem(textEnd));
                    }
                }
            }

            foreach (TagItem childTag in tagItem.ContainedTags)
            {
                this.ParseTag(childTag);
            }
        }

        /// <summary>
        /// Generate XAML from the html parse tree
        /// </summary>
        /// <param name="tagItem">the tag being processed</param>
        /// <returns>the xaml document element</returns>
        private Inline RecursiveGenerateXaml(TagItem tagItem)
        {
            // simple case - just text
            if (tagItem.ContainedTags.Count == 0)
            {
                return this.TextRun(tagItem);
            }
            
            if (tagItem.ContainedTags.Count == 1)
            {
                // simple(ish) case - just one element 
                TagItem childItem = tagItem.ContainedTags[0];
                Inline contained = this.RecursiveGenerateXaml(childItem);

                if (!string.IsNullOrEmpty(childItem.Tag))
                {
                    contained = this.tagMap.Map(childItem, contained);
                }

                return contained;
            }

            // multiple elements
            Span container = new Span();
            foreach (TagItem childItem in tagItem.ContainedTags)
            {
                Inline contained = this.RecursiveGenerateXaml(childItem);

                if (! string.IsNullOrEmpty(childItem.Tag))
                {
                    contained = this.tagMap.Map(childItem, contained);                        
                }

                container.Inlines.Add(contained);
            }

            return container;
        }

        /// <summary>
        /// Generate a text run from the TagItem
        /// </summary>
        /// <param name="tagItem">the tag item to convert</param>
        /// <returns>the generated run</returns>
        private Run TextRun(TagItem tagItem)
        {
            string text = tagItem.TextContent;

            // decode to display &amp; as "&" etc.
            // must also decode high chars -&#1488; to "א" (aleph) etc
            /// thse are encoded twice when thay are put in XML. 
            /// I don't know why the XML parse doesn't decode once.
            text = HttpUtility.HtmlDecode(text);
            text = HttpUtility.HtmlDecode(text); 

            if (this.plaintextConverter != null)
            {
                text = this.plaintextConverter(text);
            }

            return new Run(text);
        }
    }
}
